// http://dbschools.svn.sourceforge.net/viewvc/dbschools/trunk/dbschools-music/src/main/java/com/dbschools/music/Metronome.java?revision=415

import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Synthesizer;


public class Metronome {
	
    private final int velocity = 127;
    private Thread thread;
    private final Runnable runnable = createRunnable();
    private long timeBetweenBeats;
    private MidiChannel channel = null;
    private boolean keepPlaying;
    private int note;
	
    public Metronome() {
        try {
            final Synthesizer synthesizer = MidiSystem.getSynthesizer();
            synthesizer.open();
            channel = synthesizer.getChannels()[9];
            
        } catch (MidiUnavailableException ex) {
            ex.printStackTrace();
        }
        
        setTempo(108);
        setNoteFromChoice();
    }
    
    public void setTempo(int beatsPerMinute) {
        processTempoChange(beatsPerMinute);
    }
    
    public void setNote(int note) {
        this.note = note;
    }
    
    public void stop() {
        keepPlaying = false;
        if (thread != null) {
        	thread.interrupt(); // Interrupt the sleep
        }
    }
    
    private Runnable createRunnable() {

        return new Runnable() {

            public void run() {
                final long startTime = System.currentTimeMillis();
                long wokeLateBy = 0;

                while (keepPlaying) {
                    // Someone could change note while we sleep. Make sure we 
                    // turn on and off the same note.
                    final int noteForThisBeat = note; 
                    
                    if (wokeLateBy > 10) {
                    	System.out.println("Woke late by " + wokeLateBy);
                    } else {
                        channel.noteOn(noteForThisBeat, velocity);
                    }
                    final long currentTimeBeforeSleep = System.currentTimeMillis();
                    final long currentLag = (currentTimeBeforeSleep - startTime) % timeBetweenBeats;
                    final long sleepTime = timeBetweenBeats - currentLag;
                    final long expectedWakeTime = currentTimeBeforeSleep + sleepTime;
                    try {
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ex) {

                    }
                    wokeLateBy = System.currentTimeMillis() - expectedWakeTime;
                    channel.noteOff(noteForThisBeat);
                }
            }
        };
    }

    private void processTempoChange(int beatsPerMinute) {
        timeBetweenBeats = 1000 * 60 / beatsPerMinute;
        restartAtEndOfBeatIfRunning();
    }
    
    private void restartAtEndOfBeatIfRunning() {
        if (keepPlaying) {
            keepPlaying = false;
            try {
                thread.join();
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
            startThread();
        }
    }
    
    public void startThread() {
    	if(!keepPlaying) {
	        if (channel != null) {
	            keepPlaying = true;
	            thread = new Thread(runnable, "Metronome");
	            thread.setPriority(Thread.MAX_PRIORITY);
	            thread.start();
	        }
    	}
    }
    
    void setNoteFromChoice() {
        setNote(60);
    }
}
